{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "所有与电影766相似（皮尔逊）的系数前十为：\n",
      "1.000 : Copycat (1995)\n",
      "1.000 : Usual Suspects, The (1995)\n",
      "1.000 : From Dusk Till Dawn (1996)\n",
      "1.000 : White Balloon, The (1995)\n",
      "1.000 : Taxi Driver (1976)\n",
      "1.000 : Apollo 13 (1995)\n",
      "1.000 : Net, The (1995)\n",
      "1.000 : To Wong Foo, Thanks for Everything! Julie Newmar (1995)\n",
      "1.000 : Hoop Dreams (1994)\n",
      "1.000 : I.Q. (1994)\n",
      "用户232对电影52的评分预测为  3.980443976004237\n"
     ]
    }
   ],
   "source": [
    "# encoding:utf-8\n",
    "\n",
    "import os\n",
    "import csv\n",
    "import heapq\n",
    "import math\n",
    "from operator import itemgetter\n",
    "from datetime import datetime\n",
    "from collections import defaultdict\n",
    "\n",
    "# 加载电影数据文件\n",
    "def load_reviews(path, **kwargs):\n",
    "    options = {\n",
    "        'fieldnames': ('userid', 'movieid', 'rating', 'timestamp'),\n",
    "        'delimiter': '\\t'\n",
    "    }\n",
    "    options.update(kwargs)\n",
    "    parse_date = lambda r, k: datetime.fromtimestamp(float(r[k]))\n",
    "    parse_int = lambda r, k: int(r[k])\n",
    "    with open(path, 'rt', encoding=\"utf-8\") as reviews:\n",
    "        reader = csv.DictReader(reviews, **options)\n",
    "        for row in reader:\n",
    "            row['movieid'] = parse_int(row, 'movieid')\n",
    "            row['userid'] = parse_int(row, 'userid')\n",
    "            row['rating'] = parse_int(row, 'rating')\n",
    "            row['timestamp'] = parse_date(row, 'timestamp')\n",
    "            yield row\n",
    "\n",
    "# 辅助数据导入\n",
    "def relative_path(path):\n",
    "    dirname = os.path.dirname(os.path.realpath('__file__'))\n",
    "    path = os.path.join(dirname, path)\n",
    "    return  os.path.normpath(path)\n",
    "\n",
    "# 读取电影信息\n",
    "def load_movies(path, **kwargs):\n",
    "    options = {\n",
    "        'fieldnames': ('movieid', 'title', 'release', 'video', 'url'),\n",
    "        'delimiter': '|',\n",
    "        'restkey': 'genre'\n",
    "    }\n",
    "    options.update(**kwargs)\n",
    "    parse_int = lambda r, k: int(r[k])\n",
    "    parse_date = lambda r, k: datetime.strptime(r[k], '%d-%b-%Y') if r[k] else None\n",
    "    with open(path, 'rt', encoding=\"utf-8\") as movies:\n",
    "        reader = csv.DictReader(movies, **options)\n",
    "        for row in reader:\n",
    "            row['movieid'] = parse_int(row, 'movieid')\n",
    "            # print row['movieid']\n",
    "            row['release'] = parse_date(row, 'release')\n",
    "            # print row['release']\n",
    "            # print row['video']\n",
    "            yield row\n",
    "\n",
    "# 基于物品的推荐系统，这里指的是电影\n",
    "class MovieLens(object):\n",
    "\n",
    "    def __init__(self, udata, uitem):\n",
    "        self.udata = udata\n",
    "        self.uitem = uitem\n",
    "        self.movies = {}\n",
    "        self.reviews = defaultdict(dict)\n",
    "        self.load_dataset()\n",
    "\n",
    "    # 加载数据到内存中，按ID为索引\n",
    "    def load_dataset(self):\n",
    "        for movie in load_movies(self.uitem):\n",
    "            self.movies[movie['movieid']] = movie\n",
    "\n",
    "        for review in load_reviews(self.udata):\n",
    "            self.reviews[review['userid']][review['movieid']] = review\n",
    "            # print self.reviews[review['userid']][review['movieid']]\n",
    "\n",
    "    # 检查电影是否存在\n",
    "    def reviews_for_movie(self, movieid):\n",
    "        for review in self.reviews.values():\n",
    "            if movieid in review:\n",
    "                yield review[movieid]\n",
    "\n",
    "    # 对所有的电影求平均评价分数\n",
    "    def average_reviews(self):\n",
    "        for movieid in self.movies:\n",
    "            reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))\n",
    "            average = sum(reviews) / float(len(reviews))\n",
    "            yield (movieid, average, len(reviews))  # 返回了（movieid，评分平均分，长度(即评价人数)）\n",
    "\n",
    "    # 获取电影前n的top排行\n",
    "    def top_rated(self, n=10):\n",
    "        return heapq.nlargest(n, self.bayesian_average(), key=itemgetter(1))\n",
    "\n",
    "    # 获取一个修正后的贝叶斯平均值\n",
    "    def bayesian_average(self, c=59, m=3):\n",
    "        for movieid in self.movies:\n",
    "            reviews = list(r['rating'] for r in self.reviews_for_movie(movieid))\n",
    "            average = ((c * m) + sum(reviews)) / float(c + len(reviews))\n",
    "            yield (movieid, average, len(reviews))\n",
    "\n",
    "    # 返回两部电影的交集,即两部电影在同一个人观看过的情况\n",
    "    def shared_critics(self, movieA, movieB):\n",
    "        if movieA not in self.movies:\n",
    "            raise KeyError(\"Couldn't find movie '%s' in data\" % movieA)\n",
    "        if movieB not in self.movies:\n",
    "            raise KeyError(\"Couldn't find movie '%s' in data\" % movieB)\n",
    "        criticsA = set(critic for critic in self.reviews if movieA in self.reviews[critic])\n",
    "        criticsB = set(critic for critic in self.reviews if movieB in self.reviews[critic])\n",
    "        shared = criticsA & criticsB  # 和操作\n",
    "        # 创建一个评论过的字典以返回\n",
    "        reviews = {}\n",
    "        for critic in shared:\n",
    "            reviews[critic] = (\n",
    "                self.reviews[critic][movieA]['rating'],\n",
    "                self.reviews[critic][movieB]['rating']\n",
    "            )\n",
    "        return reviews\n",
    "\n",
    "    # 通过两部电影的共同属性作为向量来计算两部电影之间的欧式距离\n",
    "    def euclidean_distance(self, criticA, criticB, prefs='movies'):\n",
    "        # 创建两部电影的交集\n",
    "        preferences = self.share_preferences(criticA, criticB)\n",
    "        # 没有则返回0\n",
    "        if len(preferences) == 0: return 0\n",
    "        # 求偏差的平方的和\n",
    "        sum_of_squares = sum([pow(a - b, 2) for a, b in preferences.values()])\n",
    "        # 修正的欧式距离，返回值的范围为[0,1]\n",
    "        return 1 / (1 + math.sqrt(sum_of_squares))\n",
    "\n",
    "    # 返回两部电影之间的皮尔逊相关系数\n",
    "    def pearson_correlation(self, criticA, criticB, prefs='movies'):\n",
    "        if prefs == 'users':\n",
    "            preferences = self.share_preferences(criticA, criticB)\n",
    "        elif prefs == 'movies':\n",
    "            preferences = self.shared_critics(criticA, criticB)\n",
    "        else:\n",
    "            raise Exception(\"No preferences of type '%s'.\" % prefs)\n",
    "        length = len(preferences)\n",
    "        if length == 0: return 0\n",
    "        # 循环处理每一部电影之间的皮尔逊相关系数\n",
    "        sumA = sumB = sumSquareA = sumSquareB = sumProducts = 0\n",
    "        for a, b in preferences.values():\n",
    "            sumA += a\n",
    "            sumB += b\n",
    "            sumSquareA += pow(a, 2)\n",
    "            sumSquareB += pow(b, 2)\n",
    "            sumProducts += a * b\n",
    "        # 计算皮尔逊系数\n",
    "        numerator = (sumProducts * length) - (sumA * sumB)\n",
    "        denominator = math.sqrt(((sumSquareA * length) - pow(sumA, 2)) * ((sumSquareB * length) - pow(sumB, 2)))\n",
    "        if denominator == 0: return 0\n",
    "        return abs(numerator / denominator)\n",
    "\n",
    "    # 返回与制定电影相似的相似的电影\n",
    "    def similar_items(self, movie, metric='eculidean', n=None):\n",
    "        metrics = {\n",
    "            'euclidean': self.euclidean_distance,\n",
    "            'pearson': self.pearson_correlation,\n",
    "        }\n",
    "        distance = metrics.get(metric, None)\n",
    "        # 解决可能出现的状况\n",
    "        if movie not in self.reviews:\n",
    "            raise KeyError(\"Unknown movie, '%s'.\" % movie)\n",
    "        if not distance or not callable(distance):\n",
    "            raise KeyError(\"Unknown or unprogrammed distance metric '%s'.\" % metric)\n",
    "        items = {}\n",
    "        for item in self.movies:\n",
    "            if item == movie:\n",
    "                continue\n",
    "            items[item] = distance(item, movie, prefs='movies')\n",
    "        if n:\n",
    "            return heapq.nlargest(n, items.items(), key=itemgetter(1))\n",
    "        return items\n",
    "\n",
    "    '''\n",
    "    预测一个用户对一部电影的评分，相当于当前用户对其他电影的评分的加权均值\n",
    "    并且权重取决于其他电影和该电影的相似程度\n",
    "    '''\n",
    "    def predict_items_recommendation(self, user, movie, metric='euclidean'):\n",
    "        movie = self.similar_items(movie, metric=metric)\n",
    "        total = 0.0\n",
    "        simsum = 0.0\n",
    "        for relmovie, similarity in movie.items():\n",
    "            if relmovie in self.reviews[user]:\n",
    "                total += similarity * self.reviews[user][relmovie]['rating']\n",
    "                simsum += similarity\n",
    "        if simsum == 0.0:\n",
    "            return 0.0\n",
    "        return total / simsum\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    data = relative_path('data/ml-100k/u.data')\n",
    "    item = relative_path('data/ml-100k/u.item')\n",
    "    model = MovieLens(data, item)\n",
    "    # 基于物品的相关预测\n",
    "    # 输出所有与电影766相似（皮尔逊）的系数\n",
    "    print (\"所有与电影766相似（皮尔逊）的系数前十为：\")\n",
    "    for movie, similarity in model.similar_items(766, 'pearson', n=10):\n",
    "        print ('%0.3f : %s' % (similarity, model.movies[movie]['title']))\n",
    "    # 输出对用户232对电影52的评分预测\n",
    "    print (\"用户232对电影52的评分预测为 \",model.predict_items_recommendation(232, 52, 'pearson'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
